from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.template import RequestContext, loader
from django.utils import timezone

from app.models import Question, Testcase, Solution
from app import datatype
from app import executor
from app import langutil

import json
import pickle
import re
import time
import types

SOL_PREFIX = 'sol_'

# No need to validate if user can modify a question here
# because it cannot be saved anyway.
# TODO: catch object doesnot exist error
def add_question(request):
	t = loader.get_template('add-question.html')
	data_types = json.dumps(datatype.getAllDataTypes());

	# Load a question if any
	q_id = request.GET.get('q_id', False)
	question = 'false'
	edit_mode = 0 #0 for new, 1 for partial, 2 for full

	if q_id:
		question = Question.objects.get(pk=q_id)
		question = question.toJSON()
		edit_mode = getEditQuestionMode(q_id)
		
	c = RequestContext(request, {
		'supported_datatype': data_types,
		'question': json.dumps(question),
		'editMode': edit_mode,
	})
	return HttpResponse(t.render(c))


def list_question(request):
    questions = Question.objects.all()
    t = loader.get_template('list-questions.html')
    c = RequestContext(request, {
        'question_list': questions,
    })
    return HttpResponse(t.render(c))

#TODO: validate user can modify the question
#TODO: validate same name variable both here and in JS
#TODO: add author info
#TODO: validate preserved words for arg name
def update_question(request):

	try:
		question = loadQuestionFromForm(request.POST)

		question.save()
		message = 'Question added/updated!'
	except ParseException as e:
		message = e.value
		
	t = loader.get_template('message.html')
	c = RequestContext(request, {
		"message": message,
	})
	return HttpResponse(t.render(c))

#TODO: validate if author can change the testcase
def update_testcase(request):
	result = {"result": "ERROR"}
	try:
		req = json.loads(request.body)
		question = Question.objects.get(pk=req['question_id'])

		testcase = loadTestcaseFrom(req, question)

		#check if testcase is already there
		#note we allow testcases with same signature but different answers
		if len(Testcase.objects.filter(
			question_id__exact=req['question_id'],
			answer__exact=testcase.answer,
			answer_provided__exact=testcase.answer_provided,
			data__exact=testcase.data)) > 0:

			result['message'] = 'Same test case already exists.'
		else:
			testcase.save()
			result = {"result": "OK"}
			#build a table row for rendering
			row = [testcase.id]
			arg_list = pickle.loads(str(question.signature))
			row.extend([req['entry'][arg[1]] for arg in arg_list])
			result['row'] = row

	except ParseException as e:
		result['message'] = e.value
		
	return HttpResponse(json.dumps(result), mimetype="application/json")


#TODO: add author info
def show_question(request):
	try:
		question_id = request.GET.get('q_id')
		question = Question.objects.get(pk=question_id)

		#function signature
		arg_list = pickle.loads(str(question.signature))

		aoColumns,aaData = getTestcaseTable(question_id, question.return_type, arg_list)

		#add testcase form HTML
		form_str = generateFormStr(arg_list, question.id, question.return_type)

		# Load solutions
		sol_dict = getAllSolutions(question_id)
		
		t = loader.get_template('show-question.html')
		c = RequestContext(request, {
			'question': question,
			'aaData': aaData,
			'aoColumns': aoColumns,
			'formStr': form_str,
			'signature': arg_list,
			'solutions': json.dumps(sol_dict, sort_keys=True, indent=3),
		})
		return HttpResponse(t.render(c))

	except ObjectDoesNotExist:
		message = "Question [ID={0}] or one of its argument does not exist.".format(question_id)
		t = loader.get_template('message.html')
		c = RequestContext(request, {
			"message": message,
		})
		return HttpResponse(t.render(c))


def add_solution(request):
	try:
		# Load question
		question_id = request.GET.get('q_id')
		question = Question.objects.get(pk=question_id)

		arg_list = pickle.loads(str(question.signature))

		# Load testcases
		aoColumns,aaData = getTestcaseTable(question_id, question.return_type, arg_list)
		
		# Load solutions
		sol_dict = getAllSolutions(question_id)

		sol_id = request.GET.get('sol_id', '')
		edit_mode = 'false'
		if SOL_PREFIX + sol_id in sol_dict:
			#TODO: check authorship
			pass
		else:
			sol_id = 'false'
			

		# Load template and return
		t = loader.get_template('add-solution.html')
		c = RequestContext(request, {
			'question': question,
			'aaData': aaData,
			'aoColumns': aoColumns,
			'signature': arg_list,
			'solutions': json.dumps(sol_dict, sort_keys=True, indent=3),
			'loadSolution': sol_id,
			'editMode': edit_mode,
		})
		return HttpResponse(t.render(c))

	except ObjectDoesNotExist:
		message = "Question [ID={0}] or one of its argument does not exist.".format(question_id)
		t = loader.get_template('message.html')
		c = RequestContext(request, {
			"message": message,
		})
		return HttpResponse(t.render(c))

# TODO: check if author can modify the solution
def update_solution(request):
	try:
		req = json.loads(request.body)
		if 'sol_id' not in req:
			resp = new_sol_new_stub(req)
		else:
			resp = check_and_update_solution(req)

	except ObjectDoesNotExist:
		resp = {'result': 'ERROR'}
		resp['message'] = 'Invalid request' # which is not always true

	return HttpResponse(json.dumps(resp), mimetype="application/json")

def run_solution(request):
	try:
		req = json.loads(request.body)['run_req']
		sol_list,q_id = loadSolutions(req['sol_ids'])
		question = Question.objects.get(pk=q_id)
		arg_list = pickle.loads(str(question.signature))
		testcase_list = loadTestcases(q_id)
		execute_result = {}

		for sol in sol_list:
			fun = getattr(langutil, 'generate_prog_' + sol.lang)
			prog = fun(arg_list, sol.program, testcase_list)
			retCode,output = executor.run(prog)
			rslt = {}

			if retCode == 'OK':
				try:
					rslt['result'] = json.loads(output)
					rslt['status'] = 'OK'
				except ValueError:
					rslt['status'] = 'Parse ERROR'
					rslt['result'] = output
			else:
					rslt['status'] = 'Execution ERROR'
					rslt['result'] = retCode

			execute_result['sol_' + str(sol.id)] = rslt

		resp = {'status': 'OK'}
		#TODO: remove this
		resp['message'] = prog
		resp['result'] = execute_result
	except ObjectDoesNotExist:
		resp = {'status': 'ERROR'}
		resp['message'] = 'Cannot find solution/testcase/problem.' 
	except ParseException:
		resp = {'status': 'ERROR'}
		resp['message'] = 'Not all solutions are for the same question.' 

	return HttpResponse(json.dumps(resp), mimetype="application/json")

def lost(request):
	t = loader.get_template('message.html')
	c = RequestContext(request, {
		"message": 'So... do we have a heading?',
	})
	return HttpResponse(t.render(c))

def show_instruction(request):
	t = loader.get_template('instruction.html')
	c = RequestContext(request, {
	})
	return HttpResponse(t.render(c))

def show_contact(request):
	t = loader.get_template('contact.html')
	c = RequestContext(request, {
	})
	return HttpResponse(t.render(c))

def show_index(request):
	t = loader.get_template('index.html')
	c = RequestContext(request, {
	})
	return HttpResponse(t.render(c))


###################################################
# Helper Functions
###################################################

class ParseException(Exception):
	def __init__(self, value):
		self.value = value
	def __str__(self):
		return repr(self.value)

def stringInRange(str, a, b):
	return len(str) >= a and len(str) <= b

def loadQuestionFromForm(dict):
	q = Question()
	if 'question_id' in dict:
		q = Question.objects.get(pk=dict['question_id'])

	q.title = dict['title']
	q.description = dict['description']
	q.return_type = dict['return_type']
	q.last_update=timezone.now()
	if not q.id:
		q.time_created = timezone.now()

	if not stringInRange(q.title, 5, 128):
		raise ParseException('Invalid title.')

	if not stringInRange(q.description, 50, 1024):
		raise ParseException('Invalid description.')

	if not datatype.isValid(q.return_type):
		raise ParseException('Invalid return type.')

	pattern = re.compile(r"^[_A-Za-z]\w{0,15}$")
	arg_list = []

	for str in dict['arguments'].split(','):
		arr = str.split()
		if	len(arr) != 2:
			raise ParseException('Invalid function signature.')

		type = arr[0].strip()
		name = arr[1].strip()

		if not datatype.isValid(type) or not re.match(pattern, name):
			raise ParseException('Invalid function signature.')

		arg_list.append((type,name))

	q.signature = pickle.dumps(arg_list)
	return q

def generateFormStr(arg_list, q_id, return_type):
	s = ''
	for (type, name) in arg_list:
		
		tmp = "<p><span>{0} {1}</span><input type='text' id='t_{1}' name='t_{1}'></p>\n"

		if type[-2:] == '[]':
			tmp = "<p><span>{0} {1}</span><textarea id='t_{1}'></textarea></p>\n"
		s += tmp.format(type, name)


	tmp = """<p><span>
	<span class="t_answer_label">{0} Answer</span>
	<input type="checkbox" id="t_provide_answer" name="t_provide_answer" checked="checked">
	</span>\n"""
	if return_type[-2:] == '[]':
		tmp += "<textarea id='t_return'></textarea></p>\n"
	else:
		tmp += "<input type='text' id='t_return' name='t_return'></p>\n"
	s += tmp.format(return_type)

	s += "<input type='hidden' name='question_id' id='question_id' value='{0}'>\n".format(q_id)

	return s

def loadTestcaseFrom(req, question):

	signature = pickle.loads(str(question.signature))
	validateTestcase(signature, req['entry'])
	testcase = Testcase()
	testcase.question = question
	testcase.data = pickle.dumps(req['entry'])

	if req['answer_provided']:
		if validateByType(question.return_type, req['answer']):
			testcase.answer = req['answer']
			testcase.answer_provided = True
		else:
			raise ParseException('Invalid {0} for {1}'
				.format(req['answer'], question.return_type))
	else:
		testcase.answer_provided = False

	testcase.last_update=timezone.now()
	return testcase

def validateTestcase(signature, arg_map):
	for (arg_type,arg_name) in signature:
		if arg_name not in arg_map:
			raise ParseException(arg_name + ' is missing in request.')
		if not validateByType(arg_type, arg_map[arg_name]):
			raise ParseException('Failed to parse: ' + arg_name)

	if len(arg_map) != len(signature):
		raise ParseException('Unnecessary arguments.')

def validateByType(arg_type, oJson):

	if arg_type == 'Integer':
		return type(oJson) is types.IntType	
	if arg_type == 'Boolean':
		return type(oJson) is types.BooleanType 
	if arg_type == 'Double':
		return type(oJson) is types.FloatType or \
			type(oJson) is types.IntType
	if arg_type == 'String':
		return type(oJson) is types.StringType or \
			type(oJson) is types.UnicodeType

	if arg_type[-2:] != '[]':
		raise ParseException('Invalid arg_type: ' + type)

	currentType = arg_type[0:-2]
	if len(oJson) == 0:
		return True

	for v in oJson:
		if not validateByType(currentType, v):
			return False

	return True

def getTestcaseTableRow(testcase, arg_list):
	entry = pickle.loads(str(testcase.data))
	row = [testcase.id]
	row.extend([entry[arg[1]] for arg in arg_list])
	row.extend([''] * 3)	#execution results are empty
	row.append(testcase.answer)
	row.append('ME')
	row.append(time.mktime(testcase.last_update.timetuple()))
	return row

def getTestcaseTable(q_id, ret_type, arg_list):
	args = [{'sTitle' : 'id'}]

	for arg in arg_list:
		arg_type = arg[0]
		arg_name = arg[1]
		col = {'sTitle' : arg_type + ' ' + arg_name}
		col['semanticType'] = arg_type
		args.append(col)
		
	col = {'sTitle' : 'My Result', 'semanticType': ret_type, 'meta': 'my_sol'}
	args.append(col)
	col = {'sTitle' : 'Ref1 Result', 'semanticType': ret_type, 'meta': 'ref_sol_1'}
	args.append(col)
	col = {'sTitle' : 'Ref2 Result', 'semanticType': ret_type, 'meta': 'ref_sol_2'}
	args.append(col)
	col = {'sTitle' : 'Answer', 'semanticType': ret_type, 'meta': 'answer'}
	args.append(col)
	col = {'sTitle' : 'Author', 'meta': 'author'}
	args.append(col)
	col = {'sTitle' : 'Timestamp', 'meta': 'timestamp'}
	args.append(col)

	aoColumns = json.dumps(args)

	#all testcases
	testcase_list = Testcase.objects.filter(question_id__exact=q_id)
	aaData = []
	for t in testcase_list:
		row = getTestcaseTableRow(t, arg_list)
		aaData.append(row)

	aaData = json.dumps(aaData)

	return aoColumns,aaData

# Solution related
def new_sol_new_stub(req):
	lang = req['lang']
	name = req['name']

	if 'question_id' not in req or \
		'lang' not in req or \
		len(name) < 3 or len(name) > 32:
		raise ParseException('Invalid request.')

	question = Question.objects.get(pk=req['question_id'])
	arg_list = pickle.loads(str(question.signature))
	
	fun = getattr(langutil, 'generate_stub_' + lang)
	stub = fun(arg_list, question.return_type)

	sol = Solution()
	#TODO: add author info
	sol.lang = lang
	sol.last_update=timezone.now()
	sol.name = name
	sol.time_created = sol.last_update
	sol.program = stub
	sol.question = question
	sol.save()

	resp = {'result': 'OK'}
	resp['sol'] = sol.toJSON()
	return resp

def check_and_update_solution(req):
	sol = Solution.objects.get(pk=req['sol_id'])
	sol.updateFromJSON(req)
	sol.last_update=timezone.now()
	sol.save()
	resp = {'result': 'OK'}
	resp['sol'] = sol.toJSON()
	return resp

# Given a question ID, return a dict that
# have all solutions, keyed by sol ID
def getAllSolutions(question_id):
	sol_list = Solution.objects.filter(question_id__exact=question_id)

	return {SOL_PREFIX + str(s.id): s.toJSON() for s in sol_list}

def loadSolutions(sol_ids):
	sol_list = []
	for sid in sol_ids:
		sol_list.append(Solution.objects.get(pk=sid))

	question_id = sol_list[0].question_id
	for sol in sol_list:
		if sol.question_id != question_id:
			raise ParseException('Solutions are for different questions.')

	return sol_list,question_id

def loadTestcases(q_id):
	testcase_list = []

	results = Testcase.objects.filter(question_id__exact=q_id)
	for t in results:
		testcase_list.append(t.toJSON())

	return testcase_list

# Assume q_id exists, return 1 for testcase already exists,
# return 2 for no testcase
def getEditQuestionMode(q_id):
	results = Testcase.objects.filter(question_id__exact=q_id)
	if len(results) > 0:
		return 1
	return 2
	
